﻿#region Usings

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;

#endregion

namespace EFCachingProvider.Caching
{
    /// <summary>
    ///   Simple cache implementation. Not very efficient, uses simple LRU strategy.
    /// </summary>
    public sealed class InMemoryCache : ICache, IDisposable
    {
        private readonly Dictionary<string, HashSet<CacheEntry>> entitySetDependencies =
            new Dictionary<string, HashSet<CacheEntry>>();

        private readonly Dictionary<string, CacheEntry> entries = new Dictionary<string, CacheEntry>();
        private readonly ReaderWriterLockSlim entriesLock = new ReaderWriterLockSlim();
        private readonly object lruLock = new object();

        private int cacheAdds;
        private int cacheHits;
        private int cacheItemInvalidations;
        private int cacheMisses;

        // entry key -> CacheEntry

        private CacheEntry lruChainHead;
        private CacheEntry lruChainTail;

        /// <summary>
        ///   Initializes a new instance of the InMemoryCache class.
        /// </summary>
        public InMemoryCache()
            : this(Int32.MaxValue)
        {
        }

        /// <summary>
        ///   Initializes a new instance of the InMemoryCache class.
        /// </summary>
        /// <param name = "maxItems">The maximum number of items which can be stored in the cache.</param>
        public InMemoryCache(int maxItems)
        {
            MaxItems = maxItems;
            GetCurrentDate = () => DateTime.Now;
        }

        /// <summary>
        ///   Gets the number of cache hits.
        /// </summary>
        /// <value>The number of cache hits.</value>
        public int CacheHits
        {
            get { return cacheHits; }
        }

        /// <summary>
        ///   Gets the number of cache misses.
        /// </summary>
        /// <value>The number of cache misses.</value>
        public int CacheMisses
        {
            get { return cacheMisses; }
        }

        /// <summary>
        ///   Gets the number of cache adds.
        /// </summary>
        /// <value>The number of cache adds.</value>
        public int CacheItemAdds
        {
            get { return cacheAdds; }
        }

        /// <summary>
        ///   Gets the number of cache item invalidations.
        /// </summary>
        /// <value>The number of cache item invalidations.</value>
        public int CacheItemInvalidations
        {
            get { return cacheItemInvalidations; }
        }

        /// <summary>
        ///   Gets the maximum number of items the cache can hold.
        /// </summary>
        /// <value>The maximum number of items the cache can hold.</value>
        public int MaxItems { get; private set; }

        /// <summary>
        ///   Gets the number of items in the cache.
        /// </summary>
        /// <value>The number of items in the cache.</value>
        public int Count
        {
            get
            {
                entriesLock.EnterReadLock();
                var count = entries.Count;
                entriesLock.ExitReadLock();
                return count;
            }
        }

        internal Func<DateTime> GetCurrentDate { get; set; }

        internal CacheEntry LruChainHead
        {
            get { return lruChainHead; }
        }

        internal CacheEntry LruChainTail
        {
            get { return lruChainTail; }
        }

        #region ICache Members

        /// <summary>
        ///   Tries to the get entry by key.
        /// </summary>
        /// <param name = "key">The cache key.</param>
        /// <param name = "value">The retrieved value.</param>
        /// <returns>
        ///   A value of <c>true</c> if entry was found in the cache, <c>false</c> otherwise.
        /// </returns>
        public bool GetItem(string key, out object value)
        {
            CacheEntry entry;
            var currentDate = GetCurrentDate();

            entriesLock.EnterReadLock();
            var success = entries.TryGetValue(key, out entry);
            entriesLock.ExitReadLock();

            if (success)
            {
                if (currentDate >= entry.ExpirationTime)
                {
                    success = false;
                    InvalidateExpiredEntries();
                }
            }

            if (!success)
            {
                Interlocked.Increment(ref cacheMisses);
                value = null;
                return false;
            }
            else
            {
                Interlocked.Increment(ref cacheHits);
                MoveToTopOfLruChain(entry);
                entry.LastAccessTime = GetCurrentDate();
                if (entry.SlidingExpiration > TimeSpan.Zero)
                {
                    entry.ExpirationTime = GetCurrentDate().Add(entry.SlidingExpiration);
                }

                value = entry.Value;
                return true;
            }
        }

        /// <summary>
        ///   Adds the specified entry to the cache.
        /// </summary>
        /// <param name = "key">The entry key.</param>
        /// <param name = "value">The entry value.</param>
        /// <param name = "dependentEntitySets">The list of dependent entity sets.</param>
        /// <param name = "slidingExpiration">The sliding expiration.</param>
        /// <param name = "absoluteExpiration">The absolute expiration.</param>
        public void PutItem(string key, object value, IEnumerable<string> dependentEntitySets,
                            TimeSpan slidingExpiration, DateTime absoluteExpiration)
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            if (dependentEntitySets == null)
            {
                throw new ArgumentNullException("dependentEntitySets");
            }

            var newEntry = new CacheEntry
                               {
                                   Key = key,
                                   KeyHashCode = key.GetHashCode(),
                                   Value = value,
                                   DependentEntitySets = dependentEntitySets,
                                   SlidingExpiration = slidingExpiration,
                                   ExpirationTime = absoluteExpiration,
                               };

            if (slidingExpiration > TimeSpan.Zero)
            {
                newEntry.ExpirationTime = GetCurrentDate().Add(slidingExpiration);
            }
            else
            {
                newEntry.ExpirationTime = absoluteExpiration;
            }

            entriesLock.EnterWriteLock();

            CacheEntry oldEntry;

            if (entries.TryGetValue(key, out oldEntry))
            {
                InvalidateSingleEntry(oldEntry);
            }

            // too many items in the cache - invalidate the last one in LRU chain
            if (entries.Count >= MaxItems)
            {
                InvalidateSingleEntry(lruChainTail);
            }

            entries.Add(key, newEntry);

            foreach (var entitySet in dependentEntitySets)
            {
                HashSet<CacheEntry> queriesDependentOnSet;

                if (!entitySetDependencies.TryGetValue(entitySet, out queriesDependentOnSet))
                {
                    queriesDependentOnSet = new HashSet<CacheEntry>();
                    entitySetDependencies[entitySet] = queriesDependentOnSet;
                }

                queriesDependentOnSet.Add(newEntry);
            }

            Interlocked.Increment(ref cacheAdds);
            MoveToTopOfLruChain(newEntry);
            newEntry.LastAccessTime = GetCurrentDate();
            entriesLock.ExitWriteLock();
        }

        /// <summary>
        ///   Invalidates all cache entries which are dependent on any of the specified entity sets.
        /// </summary>
        /// <param name = "entitySets">The entity sets.</param>
        public void InvalidateSets(IEnumerable<string> entitySets)
        {
            if (entitySets == null)
            {
                throw new ArgumentNullException("entitySets");
            }

            entriesLock.EnterWriteLock();
            foreach (var entitySet in entitySets)
            {
                HashSet<CacheEntry> dependentEntries;

                if (entitySetDependencies.TryGetValue(entitySet, out dependentEntries))
                {
                    foreach (var entry in dependentEntries.ToArray())
                    {
                        InvalidateSingleEntry(entry);
                    }
                }
            }

            entriesLock.ExitWriteLock();
        }

        /// <summary>
        ///   Invalidates cache entry with a given key.
        /// </summary>
        /// <param name = "key">The cache key.</param>
        public void InvalidateItem(string key)
        {
            entriesLock.EnterWriteLock();
            CacheEntry entry;
            if (entries.TryGetValue(key, out entry))
            {
                InvalidateSingleEntry(entry);
            }

            entriesLock.ExitWriteLock();
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        ///   Releases unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            entriesLock.Dispose();
        }

        #endregion

        private void InvalidateSingleEntry(CacheEntry entry)
        {
            RemoveFromLruChain(entry);

            Interlocked.Increment(ref cacheItemInvalidations);
            Debug.Assert(entriesLock.IsWriteLockHeld, "Must be holding write lock");
            entries.Remove(entry.Key);
            foreach (var set in entry.DependentEntitySets)
            {
                entitySetDependencies[set].Remove(entry);
            }
        }

        private void MoveToTopOfLruChain(CacheEntry entry)
        {
            lock (lruLock)
            {
                if (entry != lruChainHead)
                {
                    if (entry == lruChainTail)
                    {
                        lruChainTail = lruChainTail.PreviousEntry;
                    }

                    if (entry.PreviousEntry != null)
                    {
                        entry.PreviousEntry.NextEntry = entry.NextEntry;
                    }

                    if (entry.NextEntry != null)
                    {
                        entry.NextEntry.PreviousEntry = entry.PreviousEntry;
                    }

                    if (lruChainHead != null)
                    {
                        lruChainHead.PreviousEntry = entry;
                    }

                    entry.NextEntry = lruChainHead;
                    entry.PreviousEntry = null;
                    lruChainHead = entry;

                    if (lruChainTail == null)
                    {
                        lruChainTail = entry;
                    }
                }
            }
        }

        private void RemoveFromLruChain(CacheEntry entry)
        {
            lock (lruLock)
            {
                if (entry == lruChainHead)
                {
                    lruChainHead = lruChainHead.NextEntry;
                }

                if (entry.PreviousEntry != null)
                {
                    entry.PreviousEntry.NextEntry = entry.NextEntry;
                }
                else
                {
                    lruChainHead = entry.NextEntry;
                }

                if (entry.NextEntry != null)
                {
                    entry.NextEntry.PreviousEntry = entry.PreviousEntry;
                }
                else
                {
                    lruChainTail = entry.PreviousEntry;
                }
            }
        }

        private void InvalidateExpiredEntries()
        {
            entriesLock.EnterWriteLock();

            var now = GetCurrentDate();
            CacheEntry nextEntry;
            for (var entryToExpire = LruChainHead; entryToExpire != null; entryToExpire = nextEntry)
            {
                // remember this reference as the invalication function will destroy it
                nextEntry = entryToExpire.NextEntry;
                if (now >= entryToExpire.ExpirationTime)
                {
                    InvalidateSingleEntry(entryToExpire);
                }
            }

            entriesLock.ExitWriteLock();
        }

        #region Nested type: CacheEntry

        /// <summary>
        ///   Cache entry.
        /// </summary>
        internal class CacheEntry : IEquatable<CacheEntry>
        {
            internal int KeyHashCode { get; set; }

            internal string Key { get; set; }

            internal object Value { get; set; }

            internal IEnumerable<string> DependentEntitySets { get; set; }

            internal TimeSpan SlidingExpiration { get; set; }

            internal DateTime ExpirationTime { get; set; }

            internal DateTime LastAccessTime { get; set; }

            internal CacheEntry NextEntry { get; set; }

            internal CacheEntry PreviousEntry { get; set; }

            #region IEquatable<CacheEntry> Members

            /// <summary>
            ///   Determines whether the specified <see cref = "CacheEntry" /> is equal to this instance.
            /// </summary>
            /// <param name = "other">The other cache entry.</param>
            /// <returns>
            ///   A value of <c>true</c> if the specified cache entry is equal to this instance; otherwise, <c>false</c>.
            /// </returns>
            public bool Equals(CacheEntry other)
            {
                if (other == null)
                {
                    throw new ArgumentNullException("other");
                }

                if (KeyHashCode != other.KeyHashCode)
                {
                    return false;
                }

                return Key.Equals(other.Key, StringComparison.Ordinal);
            }

            #endregion

            /// <summary>
            ///   Determines whether the specified <see cref = "System.Object" /> is equal to this instance.
            /// </summary>
            /// <param name = "obj">The <see cref = "System.Object" /> to compare with this instance.</param>
            /// <returns>
            ///   A value of <c>true</c> if the specified <see cref = "System.Object" /> is equal to this instance; otherwise, <c>false</c>.
            /// </returns>
            /// <exception cref = "T:System.NullReferenceException">
            ///   The <paramref name = "obj" /> parameter is null.
            /// </exception>
            public override bool Equals(object obj)
            {
                var other = obj as CacheEntry;
                if (other == null)
                {
                    return false;
                }

                return Equals(other);
            }

            /// <summary>
            ///   Returns a hash code for this instance.
            /// </summary>
            /// <returns>
            ///   A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. 
            /// </returns>
            public override int GetHashCode()
            {
                return KeyHashCode;
            }
        }

        #endregion
    }
}